﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Noris.Schedule.Support.Expressions
{
    #region class SqlDataSource : generátor SQL selectu pro sekvenčně přidávané tabulky, aliasy, joiny.
    /// <summary>
    /// SqlDataSource : generátor SQL selectu pro sekvenčně přidávané tabulky, aliasy, joiny.
    /// </summary>
    public class SqlDataSource
    {
        #region Konstrukce, proměnné
        internal SqlDataSource()
        {
            this.Tables = new List<SqlDataSourceItem>();
        }
        internal SqlDataSource(string topPhrase)
        {
            this.Tables = new List<SqlDataSourceItem>();
            this.TopPhrase = topPhrase;
        }
        public override string ToString()
        {
            return this.Sql;
        }
        protected List<SqlDataSourceItem> Tables;
        #endregion
        #region Public property
        /// <summary>
        /// Fráze mezi SELECT a FIELDS, typicky TOP. Pokud bude null, neuplatní se.
        /// </summary>
        public string TopPhrase { get; set; }
        /// <summary>
        /// Hodnota Master do filtru (WHERE), spojená s db sloupcem columnMasterToPrev první přidávané tabulky
        /// </summary>
        public string InitialValueMaster { get; set; }
        /// <summary>
        /// Hodnota Entry do filtru (WHERE), spojená s db sloupcem columnEntryToPrev první přidávané tabulky
        /// </summary>
        public string InitialValueEntry { get; set; }
        /// <summary>
        /// Příznak, že objekt obsahuje dostatek dat, aby sestavil formálně správný select.
        /// Musí obsahovat něco do FIELD i FROM a WHERE.
        /// </summary>
        public bool IsCorrect
        {
            get
            {
                if (this.InitialValueMaster == null && this.InitialValueMaster == null) return false;       // Nemám filtr.
                if (this.Tables.Count == 0) return false;                                                   // Nemám tabulky.
                if (this.Tables.Count(table => (table.ColumnMasterToPrev == null && table.ColumnEntryToPrev == null)) > 0) return false;  // Existují tabulky, které se nijak nenapojují do struktury (nemají žádný column Prev).
                if (this.Tables.Count(table => table.ColumnToResult != null) == 0) return false;            // Nemám results.
                return true;       // OK
            }
        }
        #endregion
        #region Přidávání tabulek
        /// <summary>
        /// Přidá tabulku do seznamu zdrojů, navázanou pouze přes Master key.
        /// Zajistí, že se nebude joinovat 2x po sobě tatáž tabulka podle téhož sloupce 
        /// (zabrání tomuto : "... FROM lcs.doklad d1 join lcs.doklad d2 on (d2.cislo_subjektu = d1.cislo_subjektu) ...").
        /// Podrobnosti v komentáři metody.
        /// Metoda vrací alias, který je přidělen této tabulce.
        /// </summary>
        /// <remarks>
        /// Tato metoda mj zajistí, že nebudu joinovat 2× tutéž tabulku sama k sobě za stejný záznam.
        /// K tomu by došlo ve zvláštním případu:
        ///  0 Class     : Výrobní příkaz   je specifikován číslem subjektu           (výchozí bod selectu)
        ///  1 Relation  : Sklad            statický, z Výrobního příkazu na Sklad, zde doprava
        ///  2 Class     : Sklad          
        ///  3 Relation  : Sklad            statický, z Výdejky na Sklad, zde doleva, z tabulky Master
        ///  4 Class     : Výdejka        
        ///  5 Attribute : Datum dodání     z tabulky Master
        /// Provedení:
        ///   Bod 1: Vztah VP => Sklad do selectu zapojí tabulku výrobních příkazů a specifikuje sloupec doprava = "sklad"
        ///   Bod 2: nic nepřidává
        ///   Bod 3: vztah z výdejky na sklad: do selectu zapojí hlavičku výdejek a to tak, že ji doleva napojí přes sloupec "sklad" a doprava dá "číslo subjektu".
        ///   Bod 4: nic nepřidává
        ///   Bod 5: atribut se najde v tabulce Master (nikoli subjekty), rád by přidal do selectu tabulku hlaviček výdejek, podle čísla subjektu.
        ///           ==» A právě zde zafunguje antiduplicita, protože tu samou tabulku máme v poslední položce, vpravo z ní kouká "číslo subjektu", a naše ta samá tabulka by se na ní vázala zase číslem subjektu.
        ///               Nyní se do poslední tabulky pouze přidají data z columnsToRight a/nebo z columnsToResult, ale tabulka se nezmění.
        /// </remarks>
        /// <param name="tableName">Tabulka, včetně vlastníka, bez alias (to se přiděluje automaticky)</param>
        /// <param name="columnMasterToPrev">Sloupec této tabulky, který se váže k předešlé tabulce (anebo pokud je toto první tabulka, pak se tento sloupec použije do fráze WHERE). Bez názvu tabulky a bez aliasu!</param>
        /// <param name="recordFilter">Přidaný filtr (do joinu nebo do where), typicky pro číslo vztahu v dynamických vztazích.
        /// Filtr musí mít následující tvar: "{{Alias}}.cislo_vztahu=145124".
        /// Tj. nesmí obsahovat spojku AND, a nemusí (ale může) obsahovat závorky.
        /// Aby bylo možno korektně odkazovat sloupce aktuální tabulky (tableName), která bude mít v selectu svůj alias, je třeba použít uvedený token {{Alias}}.
        /// Pokud by bylo třeba se odkázat na sloupec tabulky, ke které se zdejší tabulka joinuje, je možno použít token {{PrevAlias}}.</param>
        /// <param name="columnMasterToNext">Sloupec této tabulky, který se váže k následující tabulce. Bez názvu tabulky a bez aliasu!</param>
        /// <returns></returns>
        public string AddTable(string tableName, string columnMasterToPrev, string recordFilter, string columnMasterToNext)
        {
            return this._AddTable(tableName, columnMasterToPrev, null, columnMasterToNext, null, recordFilter, null);
        }
        /// <summary>
        /// Přidá tabulku do seznamu zdrojů, navázanou pouze přes Master key.
        /// Zajistí, že se nebude joinovat 2x po sobě tatáž tabulka podle téhož sloupce 
        /// (zabrání tomuto : "... FROM lcs.doklad d1 join lcs.doklad d2 on (d2.cislo_subjektu = d1.cislo_subjektu) ...").
        /// Podrobnosti v komentáři metody.
        /// Metoda vrací alias, který je přidělen této tabulce.
        /// </summary>
        /// <remarks>
        /// Tato metoda mj zajistí, že nebudu joinovat 2× tutéž tabulku sama k sobě za stejný záznam.
        /// K tomu by došlo ve zvláštním případu:
        ///  0 Class     : Výrobní příkaz   je specifikován číslem subjektu           (výchozí bod selectu)
        ///  1 Relation  : Sklad            statický, z Výrobního příkazu na Sklad, zde doprava
        ///  2 Class     : Sklad          
        ///  3 Relation  : Sklad            statický, z Výdejky na Sklad, zde doleva, z tabulky Master
        ///  4 Class     : Výdejka        
        ///  5 Attribute : Datum dodání     z tabulky Master
        /// Provedení:
        ///   Bod 1: Vztah VP => Sklad do selectu zapojí tabulku výrobních příkazů a specifikuje sloupec doprava = "sklad"
        ///   Bod 2: nic nepřidává
        ///   Bod 3: vztah z výdejky na sklad: do selectu zapojí hlavičku výdejek a to tak, že ji doleva napojí přes sloupec "sklad" a doprava dá "číslo subjektu".
        ///   Bod 4: nic nepřidává
        ///   Bod 5: atribut se najde v tabulce Master (nikoli subjekty), rád by přidal do selectu tabulku hlaviček výdejek, podle čísla subjektu.
        ///           ==» A právě zde zafunguje antiduplicita, protože tu samou tabulku máme v poslední položce, vpravo z ní kouká "číslo subjektu", a naše ta samá tabulka by se na ní vázala zase číslem subjektu.
        ///               Nyní se do poslední tabulky pouze přidají data z columnsToRight a/nebo z columnsToResult, ale tabulka se nezmění.
        /// </remarks>
        /// <param name="tableName">Tabulka, včetně vlastníka, bez alias (to se přiděluje automaticky)</param>
        /// <param name="columnMasterToPrev">Sloupec této tabulky, který se váže k předešlé tabulce (anebo pokud je toto první tabulka, pak se tento sloupec použije do fráze WHERE). Bez názvu tabulky a bez aliasu!</param>
        /// <param name="recordFilter">Přidaný filtr (do joinu nebo do where), typicky pro číslo vztahu v dynamických vztazích.
        /// Filtr musí mít následující tvar: "{{Alias}}.cislo_vztahu=145124".
        /// Tj. nesmí obsahovat spojku AND, a nemusí (ale může) obsahovat závorky.
        /// Aby bylo možno korektně odkazovat sloupce aktuální tabulky (tableName), která bude mít v selectu svůj alias, je třeba použít uvedený token {{Alias}}.
        /// Pokud by bylo třeba se odkázat na sloupec tabulky, ke které se zdejší tabulka joinuje, je možno použít token {{PrevAlias}}.</param>
        /// <param name="columnMasterToNext">Sloupec této tabulky, který se váže k následující tabulce. Bez názvu tabulky a bez aliasu!</param>
        /// <param name="columnToResult">Sloupec této tabulky, který se vloží do fields selectu = výsledek dotazu. Bez názvu tabulky a bez aliasu!</param>
        /// <returns></returns>
        public string AddTable(string tableName, string columnMasterToPrev, string recordFilter, string columnMasterToNext, string columnToResult)
        {
            return this._AddTable(tableName, columnMasterToPrev, null, columnMasterToNext, null, recordFilter, columnToResult);
        }
        /// <summary>
        /// Doplní tabulku, která může být napojena jako položková (buď tabulku položek, nebo tabulku vztahů).
        /// Metoda vrací alias, který je přidělen této tabulce.
        /// </summary>
        /// <param name="tableName">Tabulka, včetně vlastníka, bez alias (to se přiděluje automaticky)</param>
        /// <param name="columnMasterToPrev">Sloupec Master této tabulky, který se váže k předešlé tabulce (anebo pokud je toto první tabulka, pak se tento sloupec použije do fráze WHERE). Bez názvu tabulky a bez aliasu!</param>
        /// <param name="columnEntryToPrev">Sloupec Entry této tabulky, který se váže k předešlé tabulce (anebo pokud je toto první tabulka, pak se tento sloupec použije do fráze WHERE). Bez názvu tabulky a bez aliasu!</param>
        /// <param name="columnMasterToNext">Sloupec Master této tabulky, který se váže k následující tabulce. Bez názvu tabulky a bez aliasu!</param>
        /// <param name="columnEntryToNext">Sloupec Entry této tabulky, který se váže k následující tabulce. Bez názvu tabulky a bez aliasu!</param>
        /// <param name="columnToResult">Sloupec této tabulky, který se vloží do fields selectu = výsledek dotazu. Bez názvu tabulky a bez aliasu!</param>
        /// <returns></returns>
        public string AddTableEntry(string tableName, string columnMasterToPrev, string columnEntryToPrev, string columnMasterToNext, string columnEntryToNext, string columnToResult)
        {
            return this._AddTable(tableName, columnMasterToPrev, columnEntryToPrev, columnMasterToNext, columnEntryToNext, null, columnToResult);
        }
        /// <summary>
        /// Doplní tabulku, která může být napojena jako položková (buď tabulku položek, nebo tabulku vztahů).
        /// Metoda vrací alias, který je přidělen této tabulce.
        /// </summary>
        /// <param name="tableName">Tabulka, včetně vlastníka, bez alias (to se přiděluje automaticky)</param>
        /// <param name="columnMasterToPrev">Sloupec Master této tabulky, který se váže k předešlé tabulce (anebo pokud je toto první tabulka, pak se tento sloupec použije do fráze WHERE). Bez názvu tabulky a bez aliasu!</param>
        /// <param name="columnEntryToPrev">Sloupec Entry této tabulky, který se váže k předešlé tabulce (anebo pokud je toto první tabulka, pak se tento sloupec použije do fráze WHERE). Bez názvu tabulky a bez aliasu!</param>
        /// <param name="columnMasterToNext">Sloupec Master této tabulky, který se váže k následující tabulce. Bez názvu tabulky a bez aliasu!</param>
        /// <param name="columnEntryToNext">Sloupec Entry této tabulky, který se váže k následující tabulce. Bez názvu tabulky a bez aliasu!</param>
        /// <param name="recordFilter">Přidaný filtr (do joinu nebo do where), typicky pro číslo vztahu v dynamických vztazích.
        /// Filtr musí mít následující tvar: "{{Alias}}.cislo_vztahu=145124".
        /// Tj. nesmí obsahovat spojku AND, a nemusí (ale může) obsahovat závorky.
        /// Aby bylo možno korektně odkazovat sloupce aktuální tabulky (tableName), která bude mít v selectu svůj alias, je třeba použít uvedený token {{Alias}}.
        /// Pokud by bylo třeba se odkázat na sloupec tabulky, ke které se zdejší tabulka joinuje, je možno použít token {{PrevAlias}}.</param>
        /// <param name="columnToResult">Sloupec této tabulky, který se vloží do fields selectu = výsledek dotazu. Bez názvu tabulky a bez aliasu!</param>
        /// <returns></returns>
        public string AddTableEntry(string tableName, string columnMasterToPrev, string columnEntryToPrev, string columnMasterToNext, string columnEntryToNext, string recordFilter, string columnToResult)
        {
            return this._AddTable(tableName, columnMasterToPrev, columnEntryToPrev, columnMasterToNext, columnEntryToNext, recordFilter, columnToResult);
        }
        /// <summary>
        /// Doplní tabulku, která může být napojena jako položková (buď tabulku položek, nebo tabulku vztahů).
        /// Metoda vrací alias, který je přidělen této tabulce.
        /// </summary>
        /// <param name="tableName">Tabulka, včetně vlastníka, bez alias (to se přiděluje automaticky)</param>
        /// <param name="columnMasterToPrev">Sloupec Master této tabulky, který se váže k předešlé tabulce (anebo pokud je toto první tabulka, pak se tento sloupec použije do fráze WHERE). Bez názvu tabulky a bez aliasu!</param>
        /// <param name="columnEntryToPrev">Sloupec Entry této tabulky, který se váže k předešlé tabulce (anebo pokud je toto první tabulka, pak se tento sloupec použije do fráze WHERE). Bez názvu tabulky a bez aliasu!</param>
        /// <param name="columnMasterToNext">Sloupec Master této tabulky, který se váže k následující tabulce. Bez názvu tabulky a bez aliasu!</param>
        /// <param name="columnEntryToNext">Sloupec Entry této tabulky, který se váže k následující tabulce. Bez názvu tabulky a bez aliasu!</param>
        /// <param name="recordFilter">Přidaný filtr (do joinu nebo do where), typicky pro číslo vztahu v dynamických vztazích.</param>
        /// <param name="columnToResult">Sloupec této tabulky, který se vloží do fields selectu = výsledek dotazu. Bez názvu tabulky a bez aliasu!</param>
        /// <returns></returns>
        private string _AddTable(string tableName, string columnMasterToPrev, string columnEntryToPrev, string columnMasterToNext, string columnEntryToNext, string recordFilter, string columnToResult)
        {
            if ((tableName == null) || (columnMasterToPrev == null && columnEntryToPrev == null))
                return null;

            SqlDataSourceItem prev = null;
            bool merge = false;
            if (this.Tables.Count > 0)
            {   // Kontrola abych neměl 2x po sobě totéž:
                // Typicky bráním selectu: SELECT * FROM lcs.doklad d1 join lcs.doklad d2 on (d2.cislo_subjektu = d1.cislo_subjektu):
                prev = this.Tables[this.Tables.Count - 1];
                if (String.Equals(prev.TableName, tableName, StringComparison.InvariantCultureIgnoreCase)
                    && String.Equals(prev.ColumnMasterToNext, columnMasterToPrev, StringComparison.InvariantCultureIgnoreCase)
                    && ((prev.ColumnEntryToNext == null && columnEntryToPrev == null) || String.Equals(prev.ColumnEntryToNext, columnEntryToPrev, StringComparison.InvariantCultureIgnoreCase))
                    && String.Equals(prev.RecordFilter, recordFilter, StringComparison.InvariantCultureIgnoreCase)
                    && prev.ColumnEntryToNext == null)
                    merge = true;
            }

            string alias = null;
            if (merge)
            {
                alias = prev.TableAlias;
                SqlDataSourceItem table = prev;
                if (table.ColumnMasterToNext == null)
                    table.ColumnMasterToNext = columnMasterToNext;
                if (table.ColumnEntryToNext == null)
                    table.ColumnEntryToNext = columnEntryToNext;
                if (table.ColumnToResult == null)
                    table.ColumnToResult = columnToResult;
            }
            else
            {
                alias = "data" + this.Tables.Count.ToString();
                SqlDataSourceItem table = new SqlDataSourceItem(this, tableName, alias);
                table.TableName = tableName;
                table.TableAlias = alias;
                table.ColumnMasterToPrev = columnMasterToPrev;
                table.ColumnEntryToPrev = columnEntryToPrev;
                table.ColumnMasterToNext = columnMasterToNext;
                table.ColumnEntryToNext = columnEntryToNext;
                table.ColumnToResult = columnToResult;
                table.RecordFilter = recordFilter;
                this.Tables.Add(table);
            }
            return alias;
        }
        #endregion
        #region Tvorba selectu
        /// <summary>
        /// Obsahuje aktuální SQL SELECT
        /// </summary>
        public string Sql { get { return _CreateSelect(); } }
        private string _CreateSelect()
        {
            string sqlFields = "";
            string sqlFrom = "";
            string sqlWhere = "";
            for (int t = 0; t < this.Tables.Count; t++)
            {
                SqlDataSourceItem currTable = this.Tables[t];
                currTable.CreateField(ref sqlFields);
                if (t == 0)
                {
                    sqlFrom = currTable.CreateFrom();
                    sqlWhere = currTable.CreateWhere(this.InitialValueMaster, this.InitialValueEntry);
                }
                else
                {
                    SqlDataSourceItem prevTable = this.Tables[t - 1];
                    if (currTable.PrevAlias != null)
                    {
                        int prevIndex = this.Tables.FindIndex(0, t - 1, table => table.TableAlias == currTable.PrevAlias);
                        if (prevIndex >= 0)
                            prevTable = this.Tables[prevIndex];
                    }
                    sqlFrom += currTable.CreateJoin(prevTable);
                }
            }
            string topPhrase = (this.TopPhrase == null ? "" : this.TopPhrase + " ");
            return "SELECT " + topPhrase + sqlFields + sqlFrom + sqlWhere;
        }
        #endregion
        #region Protected subclass SqlDataSourceItem : data o jedné tabulce (from / join)
        /// <summary>
        /// SqlDataSourceItem : data o jedné tabulce (from / join)
        /// </summary>
        protected class SqlDataSourceItem
        {
            #region Konstrukce a property
            internal SqlDataSourceItem(SqlDataSource parent, string tableName, string tableAlias)
            {
                this.Parent = parent;
                this.TableName = tableName;
                this.TableAlias = tableAlias;
            }
            public override string ToString()
            {
                return "Table= " + TableName + "; Alias= " + TableAlias + "; Prev=" + ColumnMasterToPrev + "; Next=" + ColumnMasterToNext + "; Filter=" + RecordFilter;
            }
            protected SqlDataSource Parent;
            /// <summary>
            /// Název tabulky ("lcs.product_order")
            /// </summary>
            internal string TableName;
            /// <summary>
            /// Alias této tabulky ("data0")
            /// </summary>
            internal string TableAlias;
            /// <summary>
            /// Alias tabulky Prev. Pokud je zde null, pak je Prev tabulkou ta bezprostředně předešlá.
            /// Vyhledání konkrétní tabulky zajišťuje Parent.
            /// </summary>
            internal string PrevAlias;
            /// <summary>
            /// Sloupec této tabulky, který se váže na předešlou tabulku (subjekt)
            /// </summary>
            internal string ColumnMasterToPrev;
            /// <summary>
            /// Sloupec této tabulky, který se váže na předešlou tabulku (objekt)
            /// </summary>
            internal string ColumnEntryToPrev;
            /// <summary>
            /// Přidaný filtr záznamu této tabulky, typicky filtruje číslo vztahu v dynamických vztazích.
            /// Namísto aliasu tabulky má token {{Alias}}. Tečka není součástí aliasu, celý filtr má tedy typicky tvar: "{{Alias}}.cislo_vztahu = 123456".
            /// Pokud filtr chce použít nějaký column z prev tabulky, vloží na místo jejího aliasu token {{PrevAlias}}.
            /// </summary>
            internal string RecordFilter;
            /// <summary>
            /// Sloupec této tabulky, který typicky váže následující tabulku (subjekt)
            /// </summary>
            internal string ColumnMasterToNext;
            /// <summary>
            /// Sloupec této tabulky, který typicky váže následující tabulku (objekt)
            /// </summary>
            internal string ColumnEntryToNext;
            /// <summary>
            /// Sloupec této tabulky, který se dává do resultu (SELECT result FROM...)
            /// </summary>
            internal string ColumnToResult;
            #endregion
            #region Podpora tvorby selectu
            /// <summary>
            /// Doplní this.ColumnToResult do sqlFields
            /// </summary>
            /// <param name="sqlFields"></param>
            internal void CreateField(ref string sqlFields)
            {
                if (this.ColumnToResult == null) return;
                sqlFields += (sqlFields.Length == 0 ? "" : ", ") + this.TableAlias + "." + this.ColumnToResult;
            }
            /// <summary>
            /// Vytvoří a vrátí část "FROM table alias"
            /// </summary>
            /// <returns></returns>
            internal string CreateFrom()
            {
                string result = "";
                result = "\r\n FROM " + this.TableName + " " + this.TableAlias;
                return result;
            }
            /// <summary>
            /// Vytvoří a vrátí část "JOIN table alias ON (sloupce and filter)"
            /// </summary>
            /// <param name="prevTable"></param>
            /// <returns></returns>
            internal string CreateJoin(SqlDataSourceItem prevTable)
            {
                string result = "";
                result += "\r\n JOIN " + this.TableName + " " + this.TableAlias + " ON ";
                string join = "";
                if (this.ColumnMasterToPrev != null && prevTable.ColumnMasterToNext != null)
                    join += (join.Length == 0 ? "(" : " and ") + this.TableAlias + "." + this.ColumnMasterToPrev + "=" + prevTable.TableAlias + "." + prevTable.ColumnMasterToNext;
                if (this.ColumnEntryToPrev != null && prevTable.ColumnEntryToNext != null)
                    join += (join.Length == 0 ? "(" : " and ") + this.TableAlias + "." + this.ColumnEntryToPrev + "=" + prevTable.TableAlias + "." + prevTable.ColumnEntryToNext;
                if (this.RecordFilter != null)
                    join += (join.Length == 0 ? "(" : " and ") + this.RecordFilter.Replace("{{Alias}}", this.TableAlias).Replace("{{PrevAlias}}", prevTable.TableAlias);
                if (join.Length == 0)
                    join = "(1=0";
                result += join + ")";
                return result;
            }
            /// <summary>
            /// Vytvoří a vrátí část "WHERE alias.ColumnMasterToPrev = (valueMaster) [ and alias.ColumnEntryToPrev = (valueEntry)]
            /// </summary>
            /// <param name="valueMaster"></param>
            /// <param name="valueEntry"></param>
            /// <returns></returns>
            internal string CreateWhere(string valueMaster, string valueEntry)
            {
                string result = "";
                if (this.ColumnMasterToPrev != null && valueMaster != null)
                    result += "\r\n WHERE " + this.TableAlias + "." + this.ColumnMasterToPrev + "=" + valueMaster;
                if (this.ColumnEntryToPrev != null && valueEntry != null)
                    result += (result.Length == 0 ? "WHERE " : " and ") + this.TableAlias + "." + this.ColumnEntryToPrev + "=" + valueEntry;
                if (this.RecordFilter != null)
                    result += (result.Length == 0 ? "WHERE " : " and ") + this.RecordFilter.Replace("{{Alias}}", this.TableAlias).Replace("{{PrevAlias}}", "");
                if (result.Length == 0) return "WHERE (1=0)";
                return result;
            }
            #endregion
        }
        #endregion
    }
    #endregion
}
